<!DOCTYPE HTML>
<HTML>

<TITLE>飘落的花瓣</TITLE>
<META NAME="Generator" CONTENT="EditPlus">
<META NAME="Author" CONTENT="">
<META NAME="Keywords" CONTENT="">
<META NAME="Description" CONTENT="">
<style>
  html,
  body {
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
    overflow: hidden;
  }

  .container {
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
    background-color: #000000;
  }
</style>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
</HEAD>

<BODY>

<div id="jsi-cherry-container" class="container">
  <h1 style="color: red;">888</h1>
</div>
<script>
  var RENDERER = {
    INIT_CHERRY_BLOSSOM_COUNT: 30,
    MAX_ADDING_INTERVAL: 10,

    init: function () {
      this.setParameters();
      this.reconstructMethods();
      this.createCherries();
      this.render();
    },
    setParameters: function () {
      this.$container = $('#jsi-cherry-container');
      this.width = this.$container.width();
      this.height = this.$container.height();
      this.context = $('<canvas />').attr({ width: this.width, height: this.height }).appendTo(this.$container).get(0).getContext('2d');
      this.cherries = [];
      this.maxAddingInterval = Math.round(this.MAX_ADDING_INTERVAL * 1000 / this.width);
      this.addingInterval = this.maxAddingInterval;
    },
    reconstructMethods: function () {
      this.render = this.render.bind(this);
    },
    createCherries: function () {
      for (var i = 0, length = Math.round(this.INIT_CHERRY_BLOSSOM_COUNT * this.width / 1000); i < length; i++) {
        this.cherries.push(new CHERRY_BLOSSOM(this, true));
      }
    },
    render: function () {
      requestAnimationFrame(this.render);
      this.context.clearRect(0, 0, this.width, this.height);

      this.cherries.sort(function (cherry1, cherry2) {
        return cherry1.z - cherry2.z;
      });
      for (var i = this.cherries.length - 1; i >= 0; i--) {
        if (!this.cherries[i].render(this.context)) {
          this.cherries.splice(i, 1);
        }
      }
      if (--this.addingInterval == 0) {
        this.addingInterval = this.maxAddingInterval;
        this.cherries.push(new CHERRY_BLOSSOM(this, false));
      }
    }
  };
  var CHERRY_BLOSSOM = function (renderer, isRandom) {
    this.renderer = renderer;
    this.init(isRandom);
  };
  CHERRY_BLOSSOM.prototype = {
    FOCUS_POSITION: 300,
    FAR_LIMIT: 600,
    MAX_RIPPLE_COUNT: 100,
    RIPPLE_RADIUS: 100,
    SURFACE_RATE: 0.5,
    SINK_OFFSET: 20,

    init: function (isRandom) {
      this.x = this.getRandomValue(-this.renderer.width, this.renderer.width);
      this.y = isRandom ? this.getRandomValue(0, this.renderer.height) : this.renderer.height * 1.5;
      this.z = this.getRandomValue(0, this.FAR_LIMIT);
      this.vx = this.getRandomValue(-2, 2);
      this.vy = -2;
      this.theta = this.getRandomValue(0, Math.PI * 2);
      this.phi = this.getRandomValue(0, Math.PI * 2);
      this.psi = 0;
      this.dpsi = this.getRandomValue(Math.PI / 600, Math.PI / 300);
      this.opacity = 0;
      this.endTheta = false;
      this.endPhi = false;
      this.rippleCount = 0;

      var axis = this.getAxis(),
              theta = this.theta + Math.ceil(-(this.y + this.renderer.height * this.SURFACE_RATE) / this.vy) * Math.PI / 500;
      theta %= Math.PI * 2;

      this.offsetY = 40 * ((theta <= Math.PI / 2 || theta >= Math.PI * 3 / 2) ? -1 : 1);
      this.thresholdY = this.renderer.height / 2 + this.renderer.height * this.SURFACE_RATE * axis.rate;
      this.entityColor = this.renderer.context.createRadialGradient(0, 40, 0, 0, 40, 80);
      this.entityColor.addColorStop(0, 'hsl(330, 70%, ' + 50 * (0.3 + axis.rate) + '%)');
      this.entityColor.addColorStop(0.05, 'hsl(330, 40%,' + 55 * (0.3 + axis.rate) + '%)');
      this.entityColor.addColorStop(1, 'hsl(330, 20%, ' + 70 * (0.3 + axis.rate) + '%)');
      this.shadowColor = this.renderer.context.createRadialGradient(0, 40, 0, 0, 40, 80);
      this.shadowColor.addColorStop(0, 'hsl(330, 40%, ' + 30 * (0.3 + axis.rate) + '%)');
      this.shadowColor.addColorStop(0.05, 'hsl(330, 40%,' + 30 * (0.3 + axis.rate) + '%)');
      this.shadowColor.addColorStop(1, 'hsl(330, 20%, ' + 40 * (0.3 + axis.rate) + '%)');
    },
    getRandomValue: function (min, max) {
      return min + (max - min) * Math.random();
    },
    getAxis: function () {
      var rate = this.FOCUS_POSITION / (this.z + this.FOCUS_POSITION),
              x = this.renderer.width / 2 + this.x * rate,
              y = this.renderer.height / 2 - this.y * rate;
      return { rate: rate, x: x, y: y };
    },
    renderCherry: function (context, axis) {
      context.beginPath();
      context.moveTo(0, 40);
      context.bezierCurveTo(-60, 20, -10, -60, 0, -20);
      context.bezierCurveTo(10, -60, 60, 20, 0, 40);
      context.fill();

      for (var i = -4; i < 4; i++) {
        context.beginPath();
        context.moveTo(0, 40);
        context.quadraticCurveTo(i * 12, 10, i * 4, -24 + Math.abs(i) * 2);
        context.stroke();
      }
    },
    render: function (context) {
      var axis = this.getAxis();

      if (axis.y == this.thresholdY && this.rippleCount < this.MAX_RIPPLE_COUNT) {
        context.save();
        context.lineWidth = 2;
        context.strokeStyle = 'hsla(0, 0%, 100%, ' + (this.MAX_RIPPLE_COUNT - this.rippleCount) / this.MAX_RIPPLE_COUNT + ')';
        context.translate(axis.x + this.offsetY * axis.rate * (this.theta <= Math.PI ? -1 : 1), axis.y);
        context.scale(1, 0.3);
        context.beginPath();
        context.arc(0, 0, this.rippleCount / this.MAX_RIPPLE_COUNT * this.RIPPLE_RADIUS * axis.rate, 0, Math.PI * 2, false);
        context.stroke();
        context.restore();
        this.rippleCount++;
      }
      if (axis.y < this.thresholdY || (!this.endTheta || !this.endPhi)) {
        if (this.y <= 0) {
          this.opacity = Math.min(this.opacity + 0.01, 1);
        }
        context.save();
        context.globalAlpha = this.opacity;
        context.fillStyle = this.shadowColor;
        context.strokeStyle = 'hsl(330, 30%,' + 40 * (0.3 + axis.rate) + '%)';
        context.translate(axis.x, Math.max(axis.y, this.thresholdY + this.thresholdY - axis.y));
        context.rotate(Math.PI - this.theta);
        context.scale(axis.rate * -Math.sin(this.phi), axis.rate);
        context.translate(0, this.offsetY);
        this.renderCherry(context, axis);
        context.restore();
      }
      context.save();
      context.fillStyle = this.entityColor;
      context.strokeStyle = 'hsl(330, 40%,' + 70 * (0.3 + axis.rate) + '%)';
      context.translate(axis.x, axis.y + Math.abs(this.SINK_OFFSET * Math.sin(this.psi) * axis.rate));
      context.rotate(this.theta);
      context.scale(axis.rate * Math.sin(this.phi), axis.rate);
      context.translate(0, this.offsetY);
      this.renderCherry(context, axis);
      context.restore();

      if (this.y <= -this.renderer.height / 4) {
        if (!this.endTheta) {
          for (var theta = Math.PI / 2, end = Math.PI * 3 / 2; theta <= end; theta += Math.PI) {
            if (this.theta < theta && this.theta + Math.PI / 200 > theta) {
              this.theta = theta;
              this.endTheta = true;
              break;
            }
          }
        }
        if (!this.endPhi) {
          for (var phi = Math.PI / 8, end = Math.PI * 7 / 8; phi <= end; phi += Math.PI * 3 / 4) {
            if (this.phi < phi && this.phi + Math.PI / 200 > phi) {
              this.phi = Math.PI / 8;
              this.endPhi = true;
              break;
            }
          }
        }
      }
      if (!this.endTheta) {
        if (axis.y == this.thresholdY) {
          this.theta += Math.PI / 200 * ((this.theta < Math.PI / 2 || (this.theta >= Math.PI && this.theta < Math.PI * 3 / 2)) ? 1 : -1);
        } else {
          this.theta += Math.PI / 500;
        }
        this.theta %= Math.PI * 2;
      }
      if (this.endPhi) {
        if (this.rippleCount == this.MAX_RIPPLE_COUNT) {
          this.psi += this.dpsi;
          this.psi %= Math.PI * 2;
        }
      } else {
        this.phi += Math.PI / ((axis.y == this.thresholdY) ? 200 : 500);
        this.phi %= Math.PI;
      }
      if (this.y <= -this.renderer.height * this.SURFACE_RATE) {
        this.x += 2;
        this.y = -this.renderer.height * this.SURFACE_RATE;
      } else {
        this.x += this.vx;
        this.y += this.vy;
      }
      return this.z > -this.FOCUS_POSITION && this.z < this.FAR_LIMIT && this.x < this.renderer.width * 1.5;
    }
  };
  $(function () {
    RENDERER.init();
  });
</script>
</BODY>

</HTML>